/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用木兰宽松许可证第2版]
 * 
 * https://zhiqim.org/project/zhiqim_components/zhiqim_upload_large.htm
 *
 * Zhiqim UploadLarge is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.uploadlarge;

import java.io.IOException;
import java.util.HashMap;

import org.zhiqim.httpd.HttpExecutor;
import org.zhiqim.httpd.HttpRequest;
import org.zhiqim.httpd.HttpResponse;
import org.zhiqim.httpd.util.Responses;
import org.zhiqim.kernel.logging.Log;
import org.zhiqim.kernel.logging.LogFactory;
import org.zhiqim.kernel.util.Files;
import org.zhiqim.kernel.util.Streams;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.Urls;
import org.zhiqim.kernel.util.Validates;
import org.zhiqim.uploadlarge.dbo.UpllChunk;
import org.zhiqim.uploadlarge.dbo.UpllFile;

/**
 * 上传服务，用于固定访问地址为/service/uploadelarge，实际处理在forward中
 *
 * @version v1.0.0 @author zouzhigang 2014-3-21 新建与整理
 */
public class ZulService implements HttpExecutor, ZulConstants
{
    private static final Log log = LogFactory.getLog(ZulService.class);
    private static final String[] FILE_DIR_NOT_FORMAT = {".", "\\", "?", ":", "*", "\'", "\"", "<", ">", "|"};
    
    @Override
    public boolean isMatch(String pathInContext)
    {
        return _PATH_SERVICE_UPLOAD_LARGE_.equals(pathInContext);
    }
    
    @Override
    public void handle(HttpRequest request, HttpResponse response) throws IOException
    {
        String sessionName = Strings.toString(request.getSessionName(), "guest");
        String uploadRootDir = Zuls.getUploadRootDir(request);
        
        /**********************************************************/
        //第一步，判断上传参数
        /**********************************************************/
        int mode = request.getHeaderInt(_X_RESPONSED_MODE_, 0);
        String fileDir = request.getHeader(_X_UPLOAD_FILE_DIR_);
        String fileName = request.getHeader(_X_UPLOAD_FILE_NAME_);
        String fileMd5 = request.getHeader(_X_UPLOAD_FILE_MD5_);
        int fileMd5Target = request.getHeaderInt(_X_UPLOAD_FILE_MD5_TARGET_, 0);
        int fileCopy = request.getHeaderInt("X-Upload-File-Copy", 0);
        long fileLength = request.getHeaderInt(_X_UPLOAD_FILE_LENGTH_);
        int chunkNum = request.getHeaderInt(_X_UPLOAD_CHUNK_NUM_);
        int chunkSize = request.getHeaderInt(_X_UPLOAD_CHUNK_SIZE_);
        int chunkNo = request.getHeaderInt(_X_UPLOAD_CHUNK_NO_);
        int contentLength = request.getContentLength();
        
        if (mode < 0 || mode > 1  || fileMd5Target < 0 || fileMd5Target > 2  || !Validates.isMD5String(fileMd5))
        {//两个值可以为空，默认0，MD5码必须是标准MD5码
            log.error("USER:%s IP:%s [UploadLarge][参数不正确][mode:%s][fileMd5Target:%s][fileMd5:%s]", sessionName, request.getRemoteAddr(), mode, fileMd5Target, fileMd5);
            response.sendError(_412_PRECONDITION_FAILED_);
            return;
        }
        
        if (fileLength == -1 || chunkNum < 1 || chunkNo < 1 || chunkSize < 1024 * 100  ||   //文件大小不能为空，碎片大小不能于小100K，碎片数和当前碎片不能<1
            contentLength <= 0 || fileLength < contentLength || chunkNum < chunkNo)         //当前碎片大小不能为空，总大小不能小于当前大小，当前碎片编号必须小于总数
        {
            log.error("USER:%s IP:%s [UploadLarge][参数不正确][fileLength:%s][contentLength:%s][chunkSize:%s][chunkNo:%s]][chunkNum:%s][%s]", sessionName, request.getRemoteAddr(), fileLength, contentLength, chunkSize, chunkNo, chunkNum, request.getUserAgent());
            response.sendError(_412_PRECONDITION_FAILED_);
            return;
        }
        
        if (contentLength > chunkSize || (chunkNo < chunkNum && contentLength != chunkSize))
        {
            log.error("USER:%s IP:%s [UploadLarge][上传文件内容长度不正确][contentLength:%s][chunkSize:%s][chunkNo:%s][chunkNum:%s][%s]", sessionName, request.getRemoteAddr(), contentLength, chunkSize, chunkNo, chunkNum, request.getUserAgent());
            response.sendError(_415_UNSUPPORTED_MEDIA_TYPE_);
            return;
        }
        
        fileDir = Urls.decodeUTF8(fileDir);
        fileDir = Strings.removeStartsWith(fileDir, "/");
        fileDir = Strings.removeEndsWith(fileDir, "/");
        fileName = Urls.decodeUTF8(fileName);
        fileMd5 = Zuls.buildFileMd5(fileMd5, fileLength);
        
        if (Validates.isStrContainStrArr(fileDir, FILE_DIR_NOT_FORMAT) ||   //文件目录不支持.\:*?'"<>|
            Validates.isEmptyBlank(fileName))                               //文件名不能为空
        {
            log.error("USER:%s IP:%s [UploadLarge][参数不正确][fileDir:%s][fileName:%s]", sessionName, request.getRemoteAddr(), fileDir, fileName);
            response.sendError(_412_PRECONDITION_FAILED_);
            return;
        }
        
        /**********************************************************/
        //第二步，判断以处理器方式时检查处理器配置参数和文件目录和文件名限制
        //1.处理器检查，必须实现Upload接口
        //2.文件根目录配置必须
        //3.对文件目录进行UTF-8解码，且必须支持创建
        //4.对文件名进行UTF-8解码
        /**********************************************************/
        
        //对参数模式、文件目录和名称等作一下处理
        if (Validates.isEmptyBlank(fileDir))
            fileDir = uploadRootDir;
        else
        {
            fileDir = Strings.addEndsWith(fileDir, "/");
            fileDir = uploadRootDir + fileDir;
            if (!Files.mkDirectory(fileDir))
            {
                log.error("USER:%s IP:%s [UploadLarge][上传的文件目录无法创建][%s]", sessionName, request.getRemoteAddr(), request.getHeader(_X_UPLOAD_FILE_DIR_));
                response.sendError(_403_FORBIDDEN_);
                return;
            }
        }
        
        if (fileName.length() > 512)
            fileName = fileName.substring(fileName.length()-512);
        
        /**********************************************************/
        //第四步，以接口方式处理，判断秒传、续传还是新传
        //1.秒传是指存在已上传的MD5相同的文件
        //2.续传是指有正在上传的MD5相同且块大小相同的碎片
        //3.新传是指没有已上传和MD5相同的文件，且没有MD5相同且块大小相同的碎片
        /**********************************************************/
        
        try
        {
            UpllFile file = ZulUploader.queryFile(fileMd5);
            if (file != null)
            {//文件已存在，秒传，返回204，头部包含URL，复制一条记录
                if (fileCopy == 0)
                    file = ZulUploader.secondSaveFile(file);
                else
                    file = ZulUploader.secondCopyFile(request, file, fileDir, fileName);
                
                String result = processCallback(request, file);
                if (result != null)
                {//回调失败
                    log.error("USER:%s IP:%s [UploadLarge][秒传][%s][业务回调处理失败：%s]", sessionName, request.getRemoteAddr(), fileMd5, result);
                    response.sendError(799, result);
                    return;
                }
                
                log.info("USER:%s IP:%s [UploadLarge][秒传][%s]", sessionName, request.getRemoteAddr(), fileMd5);
                doUploadCompleted(response, mode, file.getFileId(), file.getFileName(), file.getFileUrl(), Zuls.getFilePath(request, file));
                return;
            }
    
            UpllChunk chunk = ZulUploader.queryChunk(fileMd5, chunkSize);
            if (chunk == null)
            {//文件碎片不存在
                if (chunkNo > 1)
                {//不是第一个块拒绝操作
                    log.error("USER:%s IP:%s [UploadLarge][碎片初始编号不正确或和其他人上传同一文件时冲突]", sessionName, request.getRemoteAddr());
                    response.sendError(_400_BAD_REQUEST_);
                    return;
                }
            }
            else
            {//文件碎片已存在
                int lastChunkNo = chunk.getChunkNo();
                if (chunkNo > lastChunkNo+1)
                {//跳块拒绝操作
                    log.error("USER:%s IP:%s [UploadLarge][碎片跳编号或和其他人上传同一文件时冲突]", sessionName, request.getRemoteAddr());
                    response.sendError(_400_BAD_REQUEST_);
                    return;
                }
                
                if (chunkNo <= lastChunkNo)
                {//已有块比当前多，续传
                    log.info("USER:%s IP:%s [UploadLarge][碎片续传][%s]", sessionName, request.getRemoteAddr(), chunkNo);
                    doUploadChunk(response, mode, lastChunkNo);
                    return;
                }
            }
            
            /**********************************************************/
            //第五步，上传碎片保存到存储器
            /**********************************************************/
            byte[] data = Streams.getBytes(request.getInputStream(), contentLength);
            
            if (chunk == null)
                chunk = ZulUploader.createChunkData(data, fileDir, fileName, fileMd5, fileLength, chunkSize, chunkNum);
            else
                ZulUploader.saveChunkData(data, chunk, chunkNo);
            
            if (chunkNo < chunkNum)
            {//第一个和中间的碎片
                log.info("USER:%s IP:%s [UploadLarge][碎片上传][总:%s][第:%s][%s]", sessionName, request.getRemoteAddr(), chunkNum, chunkNo, fileMd5);
                doUploadChunk(response, mode, chunkNo);
                return;
            }
            
            //最后一个碎片，先清理碎片再保存文件
            file = ZulUploader.saveFile(chunk, uploadRootDir, fileLength, request.getContentType());
               
            /**********************************************************/
            //第六步，上传碎片保存到存储器
            /**********************************************************/
            String result = processCallback(request, file);
            if (result != null)
            {//回调失败
                log.error("USER:%s IP:%s [UploadLarge][上传完成][总:%s][最后一个碎片][%s][业务回调处理失败：%s]", sessionName, request.getRemoteAddr(), chunkNum, fileMd5, result);
                response.sendError(799, result);
                return;
            }

            //回调成功
            log.info("USER:%s IP:%s [UploadLarge][上传完成][总:%s][最后一个碎片][%s][%s]", sessionName, request.getRemoteAddr(), chunkNum, fileMd5, file.getFileName());
            doUploadCompleted(response, mode, file.getFileId(), file.getFileName(), file.getFileUrl(), Zuls.getFilePath(request, file));
        }
        catch(Exception e)
        {
            try{response.sendHeader(_500_INTERNAL_SERVER_ERROR_);}catch(Exception e2){}
            log.error(e);
        }
    }
    
    /**
     * 上传碎片返回200
     * 
     * @param response          响应
     * @param mode              响应模式,=1表示按内容发送，!=1表示按消息头发送
     * @param chunkNo           碎片编号
     * @throws IOException      可能的异常
     */
    private void doUploadChunk(HttpResponse response, int mode, int chunkNo) throws IOException
    {
        HashMap<String, String> headerMap = new HashMap<String, String>();
        headerMap.put(_X_UPLOAD_CHUNK_NO_, ""+chunkNo);
        Responses.doReturnMessage(response, mode, headerMap);
    }
    
    /**
     * 上传完成返回204
     * 
     * @param response          响应
     * @param mode              响应模式,=1表示按内容发送，!=1表示按消息头发送
     * @param fileId            文件编号
     * @param fileName          文件名
     * @param fileUrl           文件URL
     * @param fileCanonicalPath 文件绝对路径
     * @throws IOException      可能的异常
     */
    private void doUploadCompleted(HttpResponse response, int mode, String fileId, String fileName, String fileUrl, String fileCanonicalPath) throws IOException
    {
        HashMap<String, String> headerMap = new HashMap<String, String>();
        headerMap.put(_X_UPLOAD_FILE_ID_, fileId);
        headerMap.put(_X_UPLOAD_FILE_NAME_, Urls.encodeUTF8(fileName));
        headerMap.put(_X_UPLOAD_FILE_URL_, fileUrl);
        headerMap.put("X-Upload-File-Path", Urls.encodeUTF8(fileCanonicalPath));
        Responses.doReturnMessage(response, mode, headerMap);
    }
    
    /**
     * 业务回调处理
     * 
     * @param request   请求
     * @param file      文件对象
     */
    private String processCallback(HttpRequest request, UpllFile file)
    {
        String fileCanonicalPath = Zuls.getFilePath(request, file);
        ZulCallback callback = request.getContextAttribute(ZulBootstrap.class).getCallback();
        
        try
        {
            callback.process(request, file, fileCanonicalPath);
            return null;
        }
        catch (Exception e)
        {
            Zuls.deleteFile(file.getFileId(), fileCanonicalPath);
            return e.getMessage();
        }
    }
}
